/*
 * Created on Oct 8, 2012
 * Created by Paul Gardner
 * 
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package org.gudy.azureus2.core3.util;

import java.io.FileInputStream;
import java.io.IOException;

public class RARTOCDecoder {
    private DataProvider provider;

    public RARTOCDecoder(DataProvider _provider) {
        provider = _provider;
    }

    public void analyse(TOCResultHandler result_handler)

    throws IOException {
        try {
            analyseSupport(result_handler);

            result_handler.complete();

        } catch (Throwable e) {

            IOException ioe;

            if (e instanceof IOException) {

                ioe = (IOException) e;

            } else {

                ioe = new IOException("Analysis failed: " + Debug.getNestedExceptionMessage(e));
            }

            result_handler.failed(ioe);

            throw (ioe);
        }
    }

    private void analyseSupport(TOCResultHandler result_handler)

    throws IOException {
        // http://acritum.com/winrar/rar-format

        byte[] header_buffer = new byte[7]; // marker block always 7 bytes

        readFully(header_buffer);

        if (!new String(header_buffer).startsWith("Rar!")) {

            throw (new IOException("Not a rar file"));
        }

        // read archive header

        readFully(header_buffer);

        int archive_header_size = getShort(header_buffer, 5);

        if (archive_header_size > 1024) {

            throw (new IOException("Invalid archive header"));
        }

        provider.skip(archive_header_size - 7); // skip over archive header

        while (true) {

            // read next 7 bytes of header record

            int read = provider.read(header_buffer);

            if (read < 7) {

                // seen some short archive, just bail

                break;
            }

            int block_type = header_buffer[2] & 0xff;

            int entry_flags = getShort(header_buffer, 3);
            int header_size = getShort(header_buffer, 5);

            // System.out.println( "type=" + Integer.toString( block_type, 16 ) + ", flags: " + Integer.toString( entry_flags, 16 ) + ", hs=" +
            // header_size);

            if (block_type < 0x70 || block_type > 0x90) {

                throw (new IOException("invalid header, archive corrupted"));
            }

            // ignore crc in first 2 bytes

            if (block_type == 0x74) {

                boolean password = (entry_flags & 0x004) != 0;

                /*
                 * in theory if this is set then there should be an optional 4 byte ADD_SIZE here but it doesn't work out if ( ( entry_flags & 0x8000
                 * ) != 0 ){ provider.skip( 4 ); }
                 */

                byte[] buffer = new byte[25]; // read up until potential optional HIGH_PACK entries

                readFully(buffer);

                long comp_size = getInteger(buffer, 0); // pack size
                long act_size = getInteger(buffer, 4); // uncompressed size

                // 1 byte host_os
                // 4 bytes crc
                // 4 bytes file time
                // 1 byte unrar version
                // 1 byte method
                // total = 11

                int extended_length = 0;

                if ((entry_flags & 0x0100) != 0) {

                    // extended size info available

                    extended_length = 8;

                    byte[] extended_size_info = new byte[8];

                    readFully(extended_size_info);

                    comp_size |= getInteger(extended_size_info, 0) << 32;
                    act_size |= getInteger(extended_size_info, 4) << 32;
                }

                // 19 - 4-comp+4-act+11

                int name_length = getShort(buffer, 19);

                if (name_length > 32 * 1024) {

                    throw (new IOException("name length too large: " + name_length));
                }

                // 4 byte attr

                byte[] name = new byte[name_length];

                readFully(name);

                String decoded_name;

                if ((entry_flags & 0x0200) != 0) {

                    int zero_pos = -1;

                    for (int i = 0; i < name.length; i++) {

                        if (name[i] == 0) {

                            zero_pos = i;

                            break;
                        }
                    }

                    if (zero_pos == -1) {

                        decoded_name = new String(name, "UTF-8");

                    } else {

                        decoded_name = decodeName(name, zero_pos + 1);

                        if (decoded_name == null) {

                            decoded_name = new String(name, 0, zero_pos, "UTF-8");
                        }
                    }
                } else {

                    decoded_name = new String(name, "UTF-8");
                }

                if ((entry_flags & 0xe0) == 0xe0) {

                    // directory

                } else {

                    result_handler.entryRead(decoded_name, act_size, password);
                }

                provider.skip(header_size - (7 + 25 + extended_length + name_length) + comp_size);

            } else if (block_type == 0x7b) {

                // end of archive

                break;

            } else {

                provider.skip(archive_header_size - 7);

                if ((entry_flags & 0x8000) != 0) {

                    provider.skip(4);
                }
            }
        }
    }

    private String decodeName(byte[] b_data, int pos) {
        try {
            int Flags = 0;
            int FlagBits = 0;

            byte[] Name = b_data;
            byte[] EncName = b_data;
            int EncSize = b_data.length;
            int MaxDecSize = 4096;

            int[] NameW = new int[MaxDecSize];

            int EncPos = pos;
            int DecPos = 0;

            byte HighByte = EncName[EncPos++];

            while (EncPos < EncSize && DecPos < MaxDecSize) {

                if (FlagBits == 0) {

                    Flags = EncName[EncPos++];
                    FlagBits = 8;
                }

                switch ((Flags >> 6) & 0x03) {

                    case 0: {
                        NameW[DecPos++] = EncName[EncPos++] & 0xff;

                        break;
                    }
                    case 1: {
                        NameW[DecPos++] = (EncName[EncPos++] & 0xff) + ((HighByte << 8) & 0xff00);

                        break;
                    }
                    case 2: {
                        NameW[DecPos++] = (EncName[EncPos++] & 0xff) + ((EncName[EncPos++] << 8) & 0xff00);

                        break;
                    }
                    case 3: {
                        int Length = EncName[EncPos++] & 0xff;

                        if ((Length & 0x80) != 0) {

                            byte Correction = EncName[EncPos++];

                            for (Length = (Length & 0x7f) + 2; Length > 0 && DecPos < MaxDecSize; Length--, DecPos++) {

                                NameW[DecPos] = ((Name[DecPos] + Correction) & 0xff) + ((HighByte << 8) & 0xff00);
                            }
                        } else {
                            for (Length += 2; Length > 0 && DecPos < MaxDecSize; Length--, DecPos++) {

                                NameW[DecPos] = Name[DecPos] & 0xff;
                            }
                        }

                        break;
                    }
                }

                Flags <<= 2;
                FlagBits -= 2;
            }

            byte[] temp = new byte[DecPos * 2];

            for (int i = 0; i < DecPos; i++) {

                temp[i * 2] = (byte) ((NameW[i] >> 8) & 0xff);
                temp[i * 2 + 1] = (byte) ((NameW[i]) & 0xff);
            }

            return (new String(temp, "UTF-16BE"));

        } catch (Throwable e) {

            Debug.outNoStack("Failed to decode name: " + ByteFormatter.encodeString(b_data) + " - " + Debug.getNestedExceptionMessage(e));

            return (null);
        }
    }

    private void readFully(byte[] buffer)

    throws IOException {
        if (provider.read(buffer) != buffer.length) {

            throw (new IOException("unexpected end-of-file"));
        }
    }

    public interface TOCResultHandler {
        public void entryRead(String name, long size, boolean password)

        throws IOException;

        public void complete();

        public void failed(IOException error);
    }

    public interface DataProvider {
        public int read(byte[] buffer)

        throws IOException;

        public void skip(long bytes)

        throws IOException;
    }

    public static void main(String[] args) {
        try {
            final FileInputStream fis = new FileInputStream("C:\\temp\\mp.part6.rar");

            RARTOCDecoder decoder = new RARTOCDecoder(new DataProvider() {
                public int read(byte[] buffer)

                throws IOException {
                    return (fis.read(buffer));
                }

                public void skip(long bytes)

                throws IOException {
                    fis.skip(bytes);
                }
            });

            decoder.analyse(new TOCResultHandler() {
                public void entryRead(String name, long size, boolean password) {
                    System.out.println(name + ": " + size + (password ? " protected" : ""));
                }

                public void complete() {
                    System.out.println("complete");
                }

                public void failed(IOException error) {
                    System.out.println("failed: " + Debug.getNestedExceptionMessage(error));
                }
            });

        } catch (Throwable e) {

            e.printStackTrace();
        }
    }

    private static int getShort(byte[] buffer, int pos) {
        return (((buffer[pos + 1] << 8) & 0xff00) | (buffer[pos] & 0xff));
    }

    private static long getInteger(byte[] buffer, int pos) {
        return ((((buffer[pos + 3] << 24) & 0xff000000) | ((buffer[pos + 2] << 16) & 0xff0000) | ((buffer[pos + 1] << 8) & 0xff00) | (buffer[pos] & 0xff)) & 0xffffffffL);
    }
}
